/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.flink;

import com.flink.vo.BankVo;
import com.flink.vo.DiffType;
import com.flink.vo.MergeVo;
import com.flink.vo.PayOrgVo;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.TimeCharacteristic;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.co.CoProcessFunction;
import org.apache.flink.util.Collector;

/**
 * Skeleton for a Flink Streaming Job.
 *
 * <p>For a tutorial how to write a Flink streaming application, check the
 * tutorials and examples on the <a href="http://flink.apache.org/docs/stable/">Flink Website</a>.
 *
 * <p>To package your application into a JAR file for execution, run
 * 'mvn clean package' on the command line.
 *
 * <p>If you change the name of the main class (with the public static void main(String[] args))
 * method, change the respective entry in the POM.xml file (simply search for 'mainClass').
 */
public class StreamingJob {

	public static void main(String[] args) throws Exception {
		// set up the streaming execution environment
		final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

		// 将时间设置为Event Time
		env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);

		DataStreamSource<PayOrgVo> source1 = env.fromElements(new PayOrgVo("113", 1), new PayOrgVo("000", 2), new PayOrgVo("115", 33));
		DataStreamSource<BankVo> source2 = env.fromElements(new BankVo("000", 2), new BankVo("115", 333), new BankVo("114", 4));

		SingleOutputStreamOperator<MergeVo> res = source1.connect(source2)
                .keyBy(PayOrgVo::getOrderNo, BankVo::getOrderNo)
                .process(new CoProcessFunction<PayOrgVo, BankVo, MergeVo>() {

			ValueState<PayOrgVo> payState;
			ValueState<BankVo> bankState;
			ValueState<Long> timerState;

			@Override
			public void open(Configuration parameters) throws Exception {
				super.open(parameters);
				payState = getRuntimeContext().getState(
						new ValueStateDescriptor<PayOrgVo>("payState", PayOrgVo.class));
				bankState = getRuntimeContext().getState(
						new ValueStateDescriptor<BankVo>("bankState", BankVo.class));
				timerState = getRuntimeContext().getState(
						new ValueStateDescriptor<Long>("timerState", Long.class));
			}

			@Override
			public void processElement1(PayOrgVo value, Context ctx, Collector<MergeVo> out) throws Exception {
				BankVo bankVo = bankState.value();// 看看银行端的数据是否到达
				if (bankVo == null) {// 银行端的时间未到达
                    payState.update(value);// 将支付机构端的数据保存起来
                    long timer = value.getEventTime() + 10_000;// 获取到支付机构端时间的事件时间，加上10秒
                    // 注册一个定时器，即支付机构端的事件时间加10秒
                    ctx.timerService().registerEventTimeTimer(timer);
                    // 保存下这个时间，在deleteEventTimeTimer中需要
                    timerState.update(timer);
				} else {
				    // 业务逻辑，不解释了
                    DiffType diffType = null;
                    if (value.getPayment().equals(bankVo.getPayment())) {
                        diffType = DiffType.F000;
                    } else {
                        diffType = DiffType.F115;
                    }
                    // 数据输出
                    out.collect(new MergeVo(diffType, value, bankVo));

                    // 两端的数据都到齐了，把定时器删掉，避免定时器被触发
                    // 至于deleteEventTimeTimer为什么需要传入刚才注册时的时间，看看源码就知道：
                    // org.apache.flink.streaming.api.operators.InternalTimerServiceImpl#deleteEventTimeTimer
                    // 里面调用了队列的remove方法，remove对象是TimerHeapInternalTimer
                    // 看看TimerHeapInternalTimer的equals方法知道
                    ctx.timerService().deleteEventTimeTimer(timerState.value());
                    // 清空状态
                    timerState.clear();
				}
			}

			@Override
			public void processElement2(BankVo value, Context ctx, Collector<MergeVo> out) throws Exception {
				// 跟processElement1逻辑一样
			    PayOrgVo payOrgVo = payState.value();
				if (payOrgVo != null) {
				    DiffType diffType = null;
					if (value.getPayment().equals(payOrgVo.getPayment())) {
						diffType = DiffType.F000;
					} else {
					    diffType = DiffType.F115;
					}
                    out.collect(new MergeVo(diffType, payOrgVo, value));
                    ctx.timerService().deleteEventTimeTimer(timerState.value());
					timerState.clear();
				} else {
					bankState.update(value);
					long timer = value.getEventTime() + 10_000;
					ctx.timerService().registerEventTimeTimer(timer);
					timerState.update(timer);
				}
			}

			@Override
			public void onTimer(long timestamp, OnTimerContext ctx, Collector<MergeVo> out) throws Exception {
				// 定时器触发后，肯定有一端的数据在指定时间没有到达
			    PayOrgVo payOrgVo = payState.value();
				BankVo bankVo = bankState.value();
				DiffType diffType = null;
				if (payOrgVo != null) {// 支付机构有数据，银行没有
				    diffType = DiffType.F113;
				}
				if (bankVo != null) {
                    diffType = DiffType.F114;
				}
                out.collect(new MergeVo(diffType, payOrgVo, bankVo));

				// 清空状态数据
				payState.clear();
				bankState.clear();
			}
		});

		res.print();
		

		// 流处理需要自己显示调用，不然不会触发
		env.execute("Flink Streaming Java API Skeleton");
	}
}
